公式チュートリアルでNext.jsに入門してみた (2) 〜Pre-rendering、データフェッチ〜
こんにちは、CX事業本部 IoT事業部の若槻です。
フロントエンドフレームワークNext.jsへの入門のために、次の公式チュートリアルを数回のシリーズに分けてこなしていき、基本的な機能に触れていこうと思います。
やってみた
本記事(2)では、チュートリアルのうち「Pre-rendering and Data Fetching」をやっていき、Next.jsのPre-renderingおよびデータフェッチについて理解を深めていきます。
- (1) 〜アプリ新規作成、ページ遷移、スタイリング編〜
- Create a Next.js App
- Navigate Between Pages
- Assets, Metadata, and CSS
- (2) 〜Pre-rendering、データフェッチ編〜
- Pre-rendering and Data Fetching
- (3) 〜Dynamic Routes、API Routes編〜
- Dynamic Routes
- API Routes
- (4) 〜デプロイ編〜
- Deploying Your Next.js App
Pre-rendering and Data Fetching
Pre-rendering
デフォルトでは、Next.jsではすべてのページのPre-renderingが有効となっています。これによりNext.jsはPre-renderingにより各ページのHTMLを事前にサーバーサイドで生成するため、クライアントサイド(ブラウザ)ですべてのJavaScript実行を行うのに比べて、パフォーマンスやSEO(Search Engine Optimization)の面で有利となります。
生成されたHTMLはブラウザ上で最低限のJavaScriptを実行し、インタラクティブなページを完成させます。このプロセスをhydration(ハイドレーション)と呼びます。
Check That Pre-rendering Is Happening
Pre-renderingの動作を試してみます。
ブラウザのJavaScriptを無効にします。
Next.jsで構築されたWebサイトhttps://next-learn-starter.vercel.app/にアクセスすると、JavaScriptを実行せずにページを正常にレンダリングできています!
これはNext.jsがページコンテンツを静的なHTMLに変換しているからです。
Summary: Pre-rendering vs No Pre-rendering
下記は、Pre-rendering(Next.jsを使用)と、非Pre-rendering(Plain Reactを使用)の違いの概要についてです。
Pre-renderingでは、初回のHTMLのロードが素早く行われるためページの表示がすぐに行われます。その後<Link >
などのJavaScriptの実行が必要な一部要素の処理が遅延して行われます。
一方で非Pre-renderingでは、すべてのReact ComponentがJavaScript実行を待って表示されます。
https://nextjs.org/learn/basics/data-fetching/pre-rendering
Two Forms of Pre-rendering
そしてNext.jsでは2種類のPre-rendering方式をサポートしています。
- Static Generation is the pre-rendering method that generates the HTML at build time. The pre-rendered HTML is then reused on each request.
- Server-side Rendering is the pre-rendering method that generates the HTML on each request.
1つはStatic Generation(SSG)で、これはアプリケーションのHTMLの生成が1度だけ行われ、それがページリクエストのたびに再利用されます。
もう1つはServer-side Rendering(SSR)で、こちらはページリクエストのたびにHTMLが再生成されます。
https://nextjs.org/learn/basics/data-fetching/two-forms
Per-page Basis
Next.jsでは同じアプリケーション内でページごとにPre-rendering方式の選択が可能です。そのためSSGとSSRのハイブリッドな構成のアプリケーションも実装できます。
When to Use Static Generation v.s. Server-side Rendering
Next.jsでは、パフォーマンス上の理由で基本的にはSSGの使用が推奨されています。
We recommend using Static Generation (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
これはページがデータを含むかどうかに関わらずです。例えば次のようなページではSSGを使用することになります。
- マーケティングページ
- ブログ一覧
- ECサイト商品一覧
- 参考ドキュメント
一方で、次のようなデータの更新が頻繁に行われるページでは、SSRを使用して表示されるデータを常に最新に保つようにします。(もしくはPre-renderingではなくclient-side JavaScriptを使用します。)
- 動画サイト(リコメンデーションされる動画がリクエストごとに頻繁に変わる)
- SNS(常に最新の投稿をトップに表示するようにする)
Static Generation with and without Data
Static Generation with Data using getStaticProps
外部データを取得する必要があるページでもStatic Generation(SSG)は利用可能です。その場合はgetStaticProps
というasync関数を使用します。
Blog Data
それでは実際にNext.jsアプリがブログデータをフェッチする動作を実装してみます。
プロジェクトトップにposts
ディレクトリを作成します。
posts
配下に、次のブログコンテンツ(マークダウン)をpre-rendering.md
ファイルに作成します。
posts
配下に、次のブログコンテンツ(マークダウン)をssg-ssr.md
ファイルに作成します。
(エディターがマークダウンを誤認識するので画像で載せています。元データはこちら)
Implement getStaticProps
マークダウンをパースするためにgray-matter
をインストールします。
$ npm install gray-matter
プロジェクトトップにlib
ディレクトリを作成します。
lib
配下に、次のファイルposts.js
を作成します。今回はここで作成するgetSortedPostsData
を外部データをフェッチするライブラリとします。
import fs from 'fs' import path from 'path' import matter from 'gray-matter' const postsDirectory = path.join(process.cwd(), 'posts') export function getSortedPostsData() { // Get file names under /posts const fileNames = fs.readdirSync(postsDirectory) const allPostsData = fileNames.map(fileName => { // Remove ".md" from file name to get id const id = fileName.replace(/\.md$/, '') // Read markdown file as string const fullPath = path.join(postsDirectory, fileName) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Combine the data with the id return { id, ...matterResult.data } }) // Sort posts by date return allPostsData.sort(({ date: a }, { date: b }) => { if (a < b) { return 1 } else if (a > b) { return -1 } else { return 0 } }) }
pages/index.js
を次のように更新します。getStaticProps
を使用してgetSortedPostsData
からデータをフェッチして処理しています。
import Layout from '../components/layout' import utilStyles from '../styles/utils.module.css' import { getSortedPostsData } from '../lib/posts' export async function getStaticProps() { const allPostsData = getSortedPostsData() return { props: { allPostsData } } } export default function Home({ allPostsData }) { return ( <Layout home> {/* Keep the existing code here */} {/* Add this <section> tag below the existing <section> tag */} <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <h2 className={utilStyles.headingLg}>Blog</h2> <ul className={utilStyles.list}> {allPostsData.map(({ id, date, title }) => ( <li className={utilStyles.listItem} key={id}> {title} <br /> {id} <br /> {date} </li> ))} </ul> </section> </Layout> ) }
しかしここで開発サーバーを起動しようとすると次のエラーとなりました。next
コマンドがどこかに行ってしまったようです。
$ npm run dev > @ dev /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/nextjs-tutorial/nextjs-blog > next dev sh: next: command not found npm ERR! code ELIFECYCLE npm ERR! syscall spawn npm ERR! file sh npm ERR! errno ENOENT npm ERR! @ dev: `next dev` npm ERR! spawn ENOENT npm ERR! npm ERR! Failed at the @ dev script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/wakatsuki.ryuta/.npm/_logs/2022-01-03T15_01_41_996Z-debug.log
本エラーは次のブログが参考になりました。
どうやらgray-matter
をインストールした時に何かしらの影響があったようです。npm upgrade
したら開発サーバーを正常に起動できるようになりました。
すると外部データのフェッチが行われ、Pre-renderingによりブログのインデックスが表示できました!
getStaticProps Details
Development vs. Production
今回使用したgetStaticProps
ですが、development環境では(おそらくデバッグを容易にするために)リクエスト毎に実行されますが、production環境ではビルド時(HTML生成時)にのみ実行されます。
- In development (npm run dev or yarn dev), getStaticProps runs on every request.
- In production, getStaticProps runs at build time. However, this behavior can be enhanced using the fallback key returned by getStaticPaths
つまり、getStaticProps
では原則として、リクエスト毎にクエリパラメータやHTTPヘッダーを指定して、データの動的取得は行えません。よって欲しいデータのみフィルターするなどしてデータフェッチを効率化することができないです。
Fetching Data at Request Time
Using getServerSideProps
そこでgetStaticProps
の代わりに、getServerSideProps
を使用することで、ビルド時ではなくリクエスト毎に動的なデータのフェッチが可能となります。
これによりTime to first byte (TTFB)は若干遅くなりますが、フェッチ結果がCDNなどにキャッシュされなくなります。
今回は実装は試しませんが、覚えておくと使う機会はあるかと思います。
おわりに
公式チュートリアルでNext.jsに入門してみた (2) 〜Pre-rendering、データフェッチ編〜 でした。
次回はこちらです。
参考
以上